iT邦幫忙

2025 iThome 鐵人賽

DAY 15
1
Modern Web

原生元件養成計畫:Web Component系列 第 15

Day 15: Web Component 的 Form-associated

  • 分享至 

  • xImage
  •  

表單元件可以說是環繞在網頁中的各個角落,你在開發上不想遇到他都很困難。
舉凡登入畫面、購物車、基本資料又或是你平時在跟 AI 互動的輸入框,這些都是表單元件的其中之一。

表單元件,看似簡單,其實都深藏不露,一個小小的 input 就要做到很多事情,像是輸入、輸出、點擊、聚焦、錯誤處理等等的表單大小事。

關於表單元件其實要學的東西有很多,接下來的幾天,我們會針對自訂表單元件進行探討。

自訂元件裡面有 input 就可以跟表單自動產生關聯嗎?

在前面幾天,我們已經能夠建立獨立運作的 Web Component。
但當自訂的元件要和表單整合時,問題就出現了!

因為我們使用了 Shadow DOM 封裝元素:
瀏覽器不知道你這個自定元件原來是要跟表單整合的 (((゚Д゚;)))

說到這裡可能還不是很清楚,不如我們就做個實驗看看吧!

試試看:定義一個包著 input 的自訂元件

input.js

class CustomInput extends HTMLElement {
  constructor() {
    super();
    const shadow = this.attachShadow({ mode: 'open' });
    shadow.innerHTML = `<input type="text" />`;
  }
}

customElements.define('custom-input', CustomInput);

我們在外部使用,並且利用原生 form 的特性,將表單 submit,看看內容會取得什麼。

index.html

<form>
  <label>Black Pink in your</label>
  <custom-input name="answer"></custom-input>
  <label>your name</label>
  <input type="text" name="blink"/>
  <button type="submit">送出</button>
</form>

<script>
document.querySelector('form').addEventListener('submit', e => {
  e.preventDefault();

  // 我們使用 formData 來取值
  const formData = new FormData(e.target);

  console.log(formData.get('answer'));// 這裡你會發現拿到 null
  console.log(formData.get('answer2')); // 原生 input 可以正常取得值
});
</script>

input


結果發現,在一般的狀況下,外部是無法透過 formData 取得自訂表單元件內容的,因為瀏覽器只認得 原生可表單關聯元素 (form-associated elements) ,所以即使這個自訂元件已經是包了 input ,他也不是表單關聯元素的一員,自然無法取得內容。

當然,你也可以利用前面章節提到的 getter/setter 提供外部一個 get value 的方式,透過 getter 來拿到自定元件中的值。
但是身為一個表單元件,不會只有單純的取值,而是會希望這個元件能夠繼承一些表單元件的屬性、方法。

如果現在希望表單元件可以被原生的 formData 認得,那應該怎麼做呢?

如何讓自定元件與表單產生關聯?

在 HTML 規範中有提出了 Form-associated custom elements (FACE) 的概念,讓自訂元素可以登記成表單的一部分。

associated

告訴瀏覽器,請將他視為表單元件 formAssociated

我們需要使用到 formAssociated 建立自定元件與表單的關聯。這樣在外部使用時,若將自訂元素放入 <form> ,瀏覽器會自動將該元素與表單建立關聯。

新增靜態屬性,告訴瀏覽器將這個自訂元素視為表單控制的元件:

 static formAssociated = true;

使用 ElementInternals 存取方法和屬性

呼叫 attachInternals() 取得 ElementInternals 物件,將其存於變數中(ElementInternals 是瀏覽器提供的一個 API)。
這時,你就可以存取表單控制元件的方法和屬性,讓你能夠建立自訂的表單元素,並且管理表單相關功能。

  • 獲得傳值的方法 setFormValue
  • 獲得驗證支援的方法 setValidity
  • 其他表單控制元件支援的常用屬性和方法...

當你做到以上兩個步驟,你就已經將你的自訂元件告訴瀏覽器他要跟表單產生關係了!

試試看:

input.js

class CustomInput extends HTMLElement {
  static formAssociated = true;

  constructor() {
    super();
    this._internals = this.attachInternals();
    const shadowRoot = this.attachShadow({ mode: 'open' });
    const cloneNode = this.render().cloneNode(true);
    shadowRoot.appendChild(cloneNode);
  }

  connectedCallback() {
    // 因為前面有 `attachInternals` 所以可以呼叫表單元件的方法,將值寫入
    const input = this.shadowRoot.querySelector('input');
    input.addEventListener('input', e => {
      // 使用表單方法 setFormValue,同步欄位的值到表單元件中
      this._internals.setFormValue(e.target.value);
    });
  }

  render() {
    const template = document.createElement('template');
    template.innerHTML = `
      <input type="text">
    `

    return template.content;
  }
}

customElements.define('custom-input', CustomInput);

index.html

<body>
    <form>
      <custom-input name="first"></custom-input>
      <button type="submit">Submit</button>
    </form>

    <script src="input.js"></script>
    <script>
      const form = document.querySelector('form');
      form.addEventListener('submit', e => {
        e.preventDefault();
        const formData = new FormData(form);
        console.log(formData.get('first'));
      })
    </script>
  </body>

試試看:

如果我們建立的元件裡面並不是放置 <input> 欄位,而是一個用 <div> 做出來看起來像是輸入框的 input 可以嗎?

答案是可以的!只要我們有這兩個重要的關鍵 formAssociated + ElementInternals
那就直接 coding 吧!

完整程式碼如下:

class CustomInput extends HTMLElement {
  static formAssociated = true;

  constructor() {
    super();
    this._internals = this.attachInternals();
    const shadowRoot = this.attachShadow({ mode: 'open' });
    const cloneNode = this.render().cloneNode(true);
    shadowRoot.appendChild(cloneNode);
  }

  connectedCallback() {
    // 因為前面有 `attachInternals` 所以可以呼叫表單元件的方法,將值寫入
    const div = this.shadowRoot.querySelector('div');
    console.log(div);
    div.addEventListener('input', e => {
      // 使用表單方法 setFormValue,同步欄位的值到表單元件中
      this._internals.setFormValue(e.target.textContent || e.target.innerText || '');
    });
  }

  render() {
    const template = document.createElement('template');
    template.innerHTML = `
      <style>
        div {
           border: 1px solid #999999;
           border-radius: 4px;
           padding: 2px 4px;
        }
      </style>

      <!--   使用一個假的 input   -->
      <div contenteditable="true"></div>
    `

    return template.content;
  }
}

customElements.define('custom-input', CustomInput);

以上,當你在外部使用時,就算裡面是一個假的 input 也能夠在 formData 中取得內容!
試著在外部的 html 插入這個元件看看吧!

今日來個小總結

記得,要讓自訂表單元件被瀏覽器認得,有兩個關鍵:

  1. static formAssociated = true:宣告自己是表單元素
  2. this._internals = this.attachInternals():存取表單控制元件的方法和屬性。

先了解以上兩點,就能讓自訂元件正式成為表單的一員。
之後表單元件不僅僅可以傳值,還能進一步驗證、或是顯示錯誤訊息等功能,接下來就一步一步實作吧!


上一篇
Day 14: Web Component 應用- Modal 的樣式接口
系列文
原生元件養成計畫:Web Component15
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言